Skip to content

Handle errors of the user functions in loo_moment_match() - #410

Merged
jgabry merged 8 commits into
masterfrom
moment-match-errors
Oct 21, 2026
Merged

jgabry merged 8 commits into
masterfrom
moment-match-errors

Conversation

@avehtari

Copy link
Copy Markdown
Member

Fixes #274.

This PR was Claude assisted

Summary

loo_moment_match() calls log_prob_upars() and log_lik_i_upars() on transformed draws, which can be more extreme than the posterior draws, and the model code can throw an error for them. In #274, neg_binomial_2_rng in the generated quantities block overflows, because cmdstanr's log_lik_i_upars() runs the whole block to get log_lik.

The split step at the end called log_lik_i_upars() without try(), so an error there stopped loo_moment_match(). With cores > 1 and two or more observations, parallel::mclapply() returned the error as a string, and the result loop then failed with "$ operator is invalid for atomic vectors".

In the transformation loop, try() printed the error, and the loop stopped for that observation. The user got no warning that moment matching had stopped early.

Change

  • If the split step fails, the observation gets the result without the split, the same as with split = FALSE. The accuracy warning that split = FALSE gives for a large Pareto k of the self-normalized weights now also covers these observations, with a text that refers to the failed split.
  • If anything else fails for one observation, for example log_lik_i(), that observation keeps its PSIS results. A try-error from a mclapply() worker gets the same treatment.
  • The loop calls try() with silent = TRUE. One warning lists the observations for which moment matching stopped early or failed, and shows the first error message.

cmdstanr cannot avoid the error: its log_lik_i_upars() needs the generated quantities block to compute log_lik, and the block also runs the *_rng calls. Users can move the *_rng code to a standalone generated quantities run.

Tests

Four new tests in tests/testthat/test_loo_moment_matching.R use the existing test model, with a log_lik_i_upars() or log_lik_i() that fails in chosen calls:

  • The split step fails: both warnings appear, and the results equal those with split = FALSE.
  • The accuracy warning uses the new text after a failed split, and the old text otherwise.
  • The first transformation fails: the warning shows the error message, and Pareto k keeps its PSIS value.
  • log_lik_i() fails for observation 1, with k_threshold = 0 so that 30 observations go through moment matching, once with cores = 1 and once with cores = 2. Observation 1 keeps its PSIS results, and the other observations get the same results as without the error.

With the master R/loo_moment_matching.R, all four tests fail: three stop with an error, and one fails an expectation. With this PR, devtools::test() with NOT_CRAN=true passes all 1280 tests and gives no warnings (R 4.6.1, testthat 3.3.2, Linux x86_64).

@codecov-commenter

codecov-commenter commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.62921% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.61%. Comparing base (c1888ca) to head (035fab8).

Files with missing lines Patch % Lines
R/loo_moment_matching.R 96.62% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #410      +/-   ##
==========================================
+ Coverage   92.49%   92.61%   +0.11%     
==========================================
  Files          31       31              
  Lines        3146     3222      +76     
==========================================
+ Hits         2910     2984      +74     
- Misses        236      238       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

This is how benchmark results would change (along with a 95% confidence interval in relative change) if 035fab8 is merged into master:

  • ✔️loo_function: 1.61s -> 1.6s [-0.27%, +0.07%]
  • 🚀loo_matrix: 1.37s -> 1.37s [-0.57%, -0.05%]
    Further explanation regarding interpretation and methodology can be found in the documentation.

@avehtari avehtari added the bug label Oct 21, 2026

@jgabry jgabry left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put a bunch of comments, some from me and some from Codex (that I checked)

Comment thread R/loo_moment_matching.R Outdated
Comment thread R/loo_moment_matching.R Outdated
Comment thread R/loo_moment_matching.R
Comment thread R/loo_moment_matching.R Outdated
Comment thread R/loo_moment_matching.R
Comment thread tests/testthat/test_loo_moment_matching.R Outdated
@avehtari

Copy link
Copy Markdown
Member Author

Thanks, these were all good points. I changed the following.

  1. Catching every error: I narrowed the catch instead of only stopping when all observations fail. The tryCatch() around the whole loo_moment_match_i() is gone. The only user-function call in loo_moment_match_i() that was not already inside a try() is log_lik_i(), so loo_moment_match_i() now catches an error of log_lik_i() and returns the observation as failed. log_prob_upars() and log_lik_i_upars() were already caught in the transformation and split steps. A bug in loo, or the max-iterations warning under options(warn = 2), now stops loo_moment_match() with an error, as before this PR, and the warning's statement that the cause was one of the user functions is correct. I also added your check: if moment matching fails for all observations, loo_moment_match() stops with the first error message. A log_lik_i with the wrong arguments fails for every observation and now gives this error.

  2. Workers that die: NULL from mclapply() is now a failed observation with the message "a parallel worker did not return a result", as you suggested. For a try-error I did one thing differently: with the narrower catch, an error in a worker can only come from loo itself, so loo_moment_match() now stops with try_error_message() of it, as the same error does with cores = 1. The warning text names a dead worker as a possible cause.

  3. Duplicate $error checks: Done, with your block.

  4. split_failed: Done: it is set where the split error is caught.

  5. First transformation fails: Done: loo_moment_match_i() returns failed = TRUE when an error occurs before any transformation was accepted (iterind == 1), so the observation keeps its PSIS results, and the warning says so.

  6. cores = 2 on Windows: Done: the test runs cores = 2 only on other systems.

Tests:

  • "an error in the first transformation keeps the PSIS results": now with k_threshold = 0, so the other observations succeed; observation 1 gets the "failed" warning and keeps its PSIS pointwise values.
  • "moment matching stops if it fails for all observations": a failing first transformation when only one observation is moment-matched, and a log_lik_i with the wrong arguments.
  • "errors in loo itself are not reported as failed observations": a log_lik_i that returns a character string, so that loo's own exp() fails; it stops with cores = 1 and with cores = 2.
  • "a parallel worker that dies gives failed observations": on systems with forked workers, log_lik_i kills its own worker process for observation 1; the result has the warning and the PSIS values of observation 1.

@jgabry jgabry left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this looks great.

But there's one interaction I didn't anticipate when I suggested the all-fail stop that Codex pointed out:

together with failed = TRUE when the first transformation errors (line 482), it now also stops when every moment-matched observation fails at its first transformation. Often only one to three observations have high k, so a #274-style *_rng overflow on the first shift now makes loo_moment_match() stop with an error, where master returned the PSIS results. The test at line 811 asserts this case.

I think it's suggestion makes sense:

But the stop is only needed when log_lik_i() fails everywhere (e.g., wrong arguments). Could it count only the observations where log_lik_i() failed (line 291)? Then a first-transformation error would keep the PSIS results with the warning, as in your note on #274.

Comment thread NEWS.md Outdated
@avehtari

Copy link
Copy Markdown
Member Author

Good catch, changed as you suggested. The stop now counts only the observations where log_lik_i() failed. log_lik_i() is evaluated at the original draws, so an error for every observation is probably an error in the function, such as wrong arguments. An error at the first transformation keeps the PSIS results with the warning, also when it happens for every moment-matched observation. The stop message is now "'log_lik_i' failed for all observations", followed by the first error message.

The test at line 811 is split in two. The #274-style case (only observation 1 above the threshold, and its first transformation fails) now checks that pointwise and diagnostics equal the PSIS results and that the warning names observation 1 and the error. The wrong-arguments log_lik_i case still checks the stop. Both tests fail on a88923d. The full test suite passes (1299 tests, NOT_CRAN=true).

@jgabry jgabry left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That change looks good. Ready to merge when CI passes.

@jgabry

jgabry commented Oct 21, 2026

Copy link
Copy Markdown
Member

Hmm, actually the benchmark results indicate a ~15% performance regression. Let me look into that before we merge

#410 (comment)

@jgabry

jgabry commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Hmm, actually the benchmark results indicate a ~15% performance regression. Let me look into that before we merge

#410 (comment)

Not a problem. The issue is that master got faster! I just merged master into this branch and that should fix it. After the benchmarks run again we can merge. Also this PR doesn't touch anything the benchmarks measure anyway.

@jgabry
jgabry merged commit 501c624 into master Oct 21, 2026
10 checks passed
@jgabry
jgabry deleted the moment-match-errors branch October 21, 2026 20:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

neg_binomial_2_rng can obstruct loo_moment_match()

3 participants